﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

namespace Ipmce.RemoteVoting.Common.Tools.CardReader.Utils
{
	static class CryptoUtils
	{
		public static byte[] CreateRandomBytes(int count)
		{
			var rnd = new Random();
			var result = new byte[count];
			rnd.NextBytes(result);
			return result;
		}

		public static byte[] ComputeKey(IEnumerable<byte> kseed, IEnumerable<byte> c)
		{
			var conc = kseed.Concat(c).ToArray();

			var sha1 = new SHA1CryptoServiceProvider();
			var result = sha1.ComputeHash(conc).Take(16).ToArray();

			FixParity(result);

			return result;
		}

		public static byte[] Encode(byte[] data, byte[] key, byte[] iv)
		{
			return Encrypt(data, new TripleDESCryptoServiceProvider { Padding = PaddingMode.None }.CreateEncryptor(key, iv));
		}

		public static IEnumerable<byte> Decode(byte[] data, byte[] key, byte[] iv)
		{
			return Decrypt(data, new TripleDESCryptoServiceProvider { Padding = PaddingMode.None, Mode = CipherMode.CBC}.CreateDecryptor(key, iv));
		}

		public static byte[] RemovePadding(byte[] data)
		{
			var lastEight = data.Skip(data.Length - 8);
			while (lastEight.Any())
			{
				lastEight = lastEight.Skip(lastEight.ToList().FindLastIndex(b => b == 0x80));
				if (lastEight.Skip(1).Any() && lastEight.Skip(1).All(b => b == 0x00))
				{
					break;
				}
			}
			return data.Take(data.Length - lastEight.Count()).ToArray();
		}
		
		public static IEnumerable<byte> ComputeKseed(string mrz)
		{
			var sha1 = new SHA1CryptoServiceProvider();
			var hash = sha1.ComputeHash(Encoding.ASCII.GetBytes(mrz));
			return hash.Take(16).ToArray();
		}

		public static IEnumerable<byte> ComputeInBookMac(byte[] data, IEnumerable<byte> key, byte[] iv, byte[] ssc = null)
		{
			data = Padding(data);
			var ka = key.Take(8).ToArray();
			var provider = new DESCryptoServiceProvider { Padding = PaddingMode.None, Mode = CipherMode.CBC };
			var y = ssc == null ? new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } : Encrypt(ssc, provider.CreateEncryptor());
			for (var i = 0; i < data.Length / 8; i++)
			{
				var block = Xor(y, data.Skip(8 * i).Take(8).ToArray());
				y = Encrypt(block, provider.CreateEncryptor(ka, iv));
			}
			provider.Mode = CipherMode.ECB;
			y = Decrypt(y, provider.CreateDecryptor(key.Skip(8).Take(8).ToArray(), iv));
			return Encrypt(y, provider.CreateEncryptor(ka, iv));
		}

		public static byte[] Xor(IList<byte> left, IList<byte> right)
		{
			var result = new byte[left.Count];
			for (var i = 0; i < left.Count; i++)
			{
				result[i] = (byte)(left[i] ^ right[i]);
			}
			return result;
		}

		public static byte[] Padding(byte[] data)
		{
			var result = new byte[(data.Length / 8 + 1) * 8];
			data.CopyTo(result, 0);
			result[data.Length] = 0x80;
			return result;
		}

		private static byte[] Decrypt(byte[] data, ICryptoTransform decryptor)
		{
			using (var msDecrypt = new MemoryStream(data))
			using (var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
			{
				var fromEncrypt = new byte[data.Length];
				csDecrypt.Read(fromEncrypt, 0, fromEncrypt.Length);
				return fromEncrypt;
			}
		}

		private static byte[] Encrypt(byte[] data, ICryptoTransform cryptoTransform)
		{
			using (var mStream = new MemoryStream())
			using (var cStream = new CryptoStream(mStream, cryptoTransform, CryptoStreamMode.Write))
			{
				cStream.Write(data, 0, data.Length);
				cStream.FlushFinalBlock();
				return mStream.ToArray();
			}
		}

		static void FixParity(IList<byte> data)
		{
			for (var i = 0; i < data.Count; ++i)
			{
				var parity = ComputeParity(data[i]);
				if (parity % 2 == 0)
				{
					var bit = (byte)(data[i] & 0x01);
					data[i] = (byte)(bit == 0 ? data[i] + 1 : data[i] - 1);
				}
			}
		}

		private static byte ComputeParity(byte b)
		{
			byte result = 0;
			for (var i = 0; i < 8; i++)
			{
				result += (byte)(b & 0x01);
				b = (byte)(b >> 1);
			}
			return result;
		}
	}
}
